using System;

namespace Neural
{	
	
	public class Neuron
	{
		
		#region fields

		/**************************************************************/
		/*                      Fields
		/**************************************************************/

		private double m_Output;
		private double m_Prime;
		private double m_Delta;
		private int m_InputCount;
		private double[] p_Weights;
		private double[] p_Updates;
		private IActivation m_Activation;

		#endregion

		#region properties

		/**************************************************************/
		/*                      Properties
		/**************************************************************/

		/// <summary>
		/// Output of this neuron
		/// </summary>
		public double Output
		{
			get	
			{ 
				return m_Output; 
			}
		}

		/// <summary>
		/// Derivative of output
		/// </summary>
		public double Prime
		{
			get	
			{ 
				return m_Prime;	
			}
		}

		/// <summary>
		/// Current activaion function
		/// </summary>
		public virtual IActivation Activation
		{
			get
			{
				return m_Activation;
			}

			set
			{
				m_Activation = value;
			}
		}

		/// <summary>
		/// Delta for weight changes. Calculated during training
		/// </summary>
		public double Delta
		{
			get	
			{ 
				return m_Delta;	
			}

			set	
			{ 
				m_Delta = value; 
			}
		}		
		
		#endregion

		#region constructors

		/**************************************************************/
		/*                      Constructors
		/**************************************************************/
			
		public Neuron(int inputs, IActivation activation)
		{
			m_InputCount = inputs;
			m_Activation = activation;
			p_Weights = new double[m_InputCount + 1]; /* +1 for bias */
			p_Updates = new double[m_InputCount + 1]; /* +1 for bias */
		}
			
		#endregion

		#region methods

		/**************************************************************/
		/*                      Methods
		/**************************************************************/

		/// <summary>
		/// Calculates output of this neuron
		/// </summary>
		public double Calculate(double[] inputs)
		{
			if (inputs.Length != m_InputCount)
				throw new ArgumentException("Invalid input vector");
			
			/*
			 * Calculate weighted sum of inputs (Dot product of input and 
			 * weight vectors)
			 */ 
			m_Output = 0;
			for (int i = 0; i < m_InputCount; i++)
				m_Output += p_Weights[i] * inputs[i];
			m_Output += p_Weights[m_InputCount] * (-1); /* bias */
			m_Prime = m_Output;
			
			/*
			 * Calculate neuron's activation and it's derivative if
			 * activation function is available
			 */ 
			if (m_Activation != null)
			{
				m_Output = m_Activation.Output(m_Output);
				m_Prime = m_Activation.Prime(m_Output);
			}

			return m_Output;
		}

		/// <summary>
		/// Updates weight vector according to <see cref="Delta">Delta</see>
		/// This method is called during online training
		/// </summary>
		public void UpdateWeights(double[] inputs, double rate, 
			double momentum)
		{
			if (inputs.Length != m_InputCount)
				throw new ArgumentException("Invalid input vector");

			double alpha = m_Delta * rate; 
			double beta;
			
			/*
			 * Update all input connection weights acc. to 
			 * calculated delta error.
			 */ 
			for (int i = 0; i < m_InputCount; i++)
			{
				beta = alpha * inputs[i];
				p_Weights[i] += beta + momentum * p_Updates[i];
				p_Updates[i] = beta;
			}

			/*
			 * Update bias weight as well
			 */ 
			beta = alpha * (-1); /* bias */
			p_Weights[m_InputCount] += beta + momentum * p_Updates[m_InputCount];
			p_Updates[m_InputCount] = beta;

		}

		/// <summary>
		/// Collects weight changes for current epoch
		/// This method is called during batch training. Instead of updating
		/// weights after each pattern, all weights changes are collected to
		/// be applied after every pattern has been presented
		/// </summary>
		public void CollectWeights(double[] inputs, double rate)
		{
			if (inputs.Length != m_InputCount)
				throw new ArgumentException("Invalid input vector");
		
			/*
			 * Update all input connection weights acc. to 
			 * calculated delta error.
			 */ 
			for (int i = 0; i < m_InputCount; i++)
			{
				p_Updates[i] += m_Delta * inputs[i];
			}

			/*
			 * Update bias weight as well
			 */ 
			p_Updates[m_InputCount] += m_Delta * (-1);
		}

		/// <summary>
		/// Must be called before batch back-prop training
		/// </summary>
		public void ResetWeights()
		{
			for (int i = 0; i < p_Updates.Length; i++)
				p_Updates[i] = 0;
		}

		/// <summary>
		/// Must be called after batch back-prop training
		/// </summary>
		public void ApplyWeights(double rate)
		{	
			/*
			 * Update all input connection weights acc. to 
			 * previously collected weight changes
			 */ 
			for (int i = 0; i < p_Weights.Length; i++)
			{
				p_Weights[i] += rate * p_Updates[i];
			}

			/*
			 * Update bias weight as well
			 */ 
			p_Weights[m_InputCount] += rate * p_Updates[m_InputCount];
		}

		/// <summary>
		/// Returns weighted m_Delta for specific input connection
		/// </summary>
		public double GetWeightedDalta(int index)
		{
			return m_Delta * p_Weights[index];
		}

		/// <summary>
		/// Randomizes the weight vector
		/// </summary>
		/// <param name="engine">Random number generator. If not specified p_Weights are
		/// reset to zero</param>
		public void Randomize(System.Random engine)
		{
			if (engine != null)
			{
                for (int i = 0; i < p_Weights.Length; i++)
					p_Weights[i] = engine.NextDouble();
			}
			else
			{
				for (int i = 0; i < p_Weights.Length; i++)
					p_Weights[i] = 0;
			}
		}

		#endregion

	}
}
